package server.files;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.sql.*;


/**
 * Tackles low-level tasks related to keeping versions of files.
 * Updates the database, locks, unlocks, renames and moves around files.
 * @author ernesta
 */
public class SFileManager {
    private static SFileManager manager;
    private HashMap<String, String[]> fileMap;
    
    private final String DB_CLASS = "org.postgresql.Driver";
    private final String DB_URL = "jdbc:postgresql://192.168.239.10:5432/eorlov10";
    private final String DB_USER = "eorlov10";
    private final String DB_PASS = "AqVgYn08";
    
    
    /**
     * Private constructor to prevent instantiation from other classes.
     * Conforms to the singleton design pattern.
     */
    private SFileManager() {
        fileMap = new HashMap<String, String[]>();
        this.updateFileMap();
    }
    
    
    /**
     * Implementation of the singleton pattern using lazy instantiation.
     * synchronized is used for thread-safety reasons.
     * @return an instance of the SFileManager class
     */
    public static synchronized SFileManager getInstance() {
        if (manager == null) {
            manager = new SFileManager();
        }
        
        return manager;
    }
    
    
    /**
     * Prevents cloning of a singleton.
     * @return nothing is returned in order to prevent cloning
     * @throws CloneNotSupportedException 
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
    
    
    /**
     * Get time of the latest commit.
     * @param filename name of a remote file
     * @return UNIX time of the last commit of the file, or -1 for an error
     */
    public long getCommitTime(String filename) {
        return -1;
    }
    
    
    /**
     * Get time of the latest commit.
     * @param filename name of a remote file
     * @return UNIX time of the last commit of the file, or -1 for an error
     */
    public long getLockTime(String filename) {
        if (fileMap.containsKey(filename)) {
            String[] relatedData = fileMap.get(filename);
            return Integer.parseInt(relatedData[0]);
        }
        return -1;
    }
    
    
    /**
     * A getter for the author of the last commit.
     * @param filename name of the local file
     * @return username of the commit author
     */
    public String getAuthor(String filename) {
        if (fileMap.containsKey(filename)) {
            String[] relatedData = fileMap.get(filename);
            return relatedData[1];
        }
        return "";
    }
    
    
    /**
     * A getter for the version of the last commit.
     * @param filename name of the local file
     * @return the most recent version number
     */
    public int getVersion(String filename) {
        if (fileMap.containsKey(filename)) {
            String[] relatedData = fileMap.get(filename);
            return Integer.parseInt(relatedData[2]);
        }
        return -1;
    }
    
    
    /**
     * Physically lock a file.
     * @param filename of the file to lock
     * @return true for success, false for failure
     */
    public boolean lock(String filename) {
        return true;
    }
    
    
    /**
     * Physically unlock a file.
     * @param filename of the file to unlock
     */
    public void unlock(String filename) {}
    
    
    /**
     * Add file lock information to a database.
     * @param filename of the file to lock
     * @param username of the user who is locking the file
     * @return true for success, false for failure
     */
    public boolean lockFileDB(String filename, String username) {
        String query = "UPDATE Current SET state = l, user = (?), time = (?) WHERE filename = (?)";
        
        String[] attributes = new String[3];
        attributes[0] = username;
        attributes[1] = this.getCurrentTime();
        attributes[2] = filename;
        
        this.updateDatabase(query, attributes);
        this.updateFileMap();
        
        return true;
    }
    
    
    /**
     * Add a new version of a file to the database
     * @param filename
     * @param username 
     */
    public void addVersionDB(String filename, String username) {
        String query = "INSERT INTO Versions (filename, user, time, version) VALUES ((?), (?), (?), (?)";
        
        String[] attributes = new String[4];
        attributes[0] = filename;
        attributes[1] = username;
        attributes[2] = this.getCurrentTime();
        attributes[3] = Integer.toString(this.getVersion(filename) + 1);
        
        this.updateDatabase(query, attributes);
    }
    
    public void unlockFileDB(String filename, String username) {
        //Locked: l, unlocked: u
        String query = "UPDATE Current SET state = 'u', user = NULL, time = (?) WHERE filename = (?)";
        
        String[] attributes = new String[2];
        attributes[0] = this.getCurrentTime();
        attributes[1] = filename;
        
        this.updateDatabase(query, attributes);
        this.updateFileMap();
    }
    
    
    /**
     * Update the database
     * @param query as a prepared statement
     * @param attributes for the prepared statement
     */
    public void updateDatabase(String query, String[] attributes) {        
        try {
            this.queryDatabase(query, attributes);
        } catch (SQLException ex) {
            System.out.println(ex);
        }
    }
    
    
    /**
     * Rename a file to include its version number
     * @param filename of the file to be renamed
     * @param path of the file in question
     * @return the new file name
     */
    public String renameWithVersion(String filename, String path) {
        String addition = "v" + (this.getVersion(filename) + 1);
        String newFilename = filename + addition;
        
        File file = new File(path + filename);
        File newFile = new File(path + newFilename);
        file.renameTo(newFile);
        
        return newFilename;
    }
    
    
    /**
     * Move a file to a new location
     * //http://stackoverflow.com/questions/300559/move-copy-file-operations-in-java
     * @param filename of the file to move
     * @param from the source directory
     * @param to the destination directory
     * @throws IOException
     */
    public void moveFile(String filename, String from, String to) throws IOException {
        FileChannel source = null;
        FileChannel destination = null;
 
        File sourceFile = new File(from + filename);
        File destFile = new File(to + filename);
        
        try {
            source = new FileInputStream(sourceFile).getChannel();
            destination = new FileOutputStream(destFile).getChannel();
            destination.transferFrom(source, 0, source.size());
        } finally {
            if (source != null) source.close();
            if (destination != null) destination.close();
        }
    }
    
    
    /**
     * Checks if a file is physically locked
     * @param filename of the file in question
     * @return true for locked, false for unlocked
     */
    public boolean isLocked(String filename) {
        return true;
    }
    
    
    
    /**
     * Update the file map with the most recent information from the database
     */
    private void updateFileMap() {
        fileMap.clear();
        String query = "SELECT * FROM current";
        
        try {
            ResultSet result = this.queryDatabase(query, null);
            ResultSetMetaData resultMeta = result.getMetaData();
            
            int columns = resultMeta.getColumnCount();
            String[] relatedData = new String[columns - 1];
            
            while (result != null && result.next()) {
                String filename = result.getString(1); //filename
                relatedData[0] = result.getString(2); //state
                relatedData[1] = result.getString(3); //user
                relatedData[2] = String.valueOf(result.getInt(4)); //time
                relatedData[3] = String.valueOf(result.getInt(5)); //version
                
                fileMap.put(filename, relatedData);
            }
            
            result.close();
        } catch (SQLException ex) {
            System.out.println(ex);
        }
    }
    
    
    /**
     * Query the database
     * @param query as a prepared statement
     * @param attributes to be used in conjunction with the prepared statement
     * @return a set of results
     * @throws SQLException
     */
    private ResultSet queryDatabase(String query, String[] attributes) throws SQLException {
        ResultSet result = null;
        Connection connection = null;
        
        try {
            Class.forName(DB_CLASS).newInstance();
            connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS);
            connection.setAutoCommit(false);
            
            PreparedStatement statement = connection.prepareStatement(query);
            if (attributes != null) {
                for (int i = 0; i <= attributes.length; i++) {
                    statement.setString(i + 1, attributes[i]);
                }
            }
            
            result = statement.executeQuery();
            
        } catch (ClassNotFoundException ex) {
            System.out.println(ex);
        } catch (InstantiationException ex) {
            System.out.println(ex);
        } catch (IllegalAccessException ex) {
            System.out.println(ex);
        } finally {
            connection.close();
        }
        
        return result;
    }
    
    
    /**
     * A helper method for getting current time
     * @return current system time in seconds
     */
    private String getCurrentTime() {
        System.out.println(System.currentTimeMillis());
        return Long.toString(System.currentTimeMillis() / 1000L);
    }
}